#!/usr/bin/env python
"""
Shadowrun Fourth Edition initiative module.


Code license:

The contents of this file are subject to the Mozilla Public License
Version 1.1 (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.mozilla.org/MPL/

Software distributed under the License is distributed on an "AS IS"
basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the 
License for the specific language governing rights and limitations under 
the License.

The Original Code is copyright 2009 OCSrun.

The Initial Developer of the Original Code is OCSrun. All Rights Reserved.

Contributor(s): Brian C. Young, Richard Garfinkel.


Content license: Copyright 2001-2008 WizKids, Inc.

WizKids, Inc. has sole ownership of the names, logo, artwork, marks,
photographs, sounds, audio, video and/or any proprietary material used in
connection with the game Shadowrun. WizKids, Inc. has granted permission to
OCSrun to use such names, logos, artwork, marks and/or any proprietary
materials for promotional and informational purposes on its website but does
not endorse, and is not affiliated with OCSrun in any official capacity
whatsoever.
"""

from character import *
from utils import *

class SRInitTool:
    """
    A class for managing Shadowrun initiative.  Horrendoulsy inefficient
    as these things go, but not expected to handle debilitating numbers
    of characters.

    Members:
        mCharacters - set - SRCharacter objects for characters in the combat
        mRound - integer - current combat round. Starts at one and counts
            upward.
        mPass - integer - current initiative pass. Starts at zero and counts
            upward.
        mPassLists - list - a list of lists, each of which is the characters
            in an initiative pass.
        mActedLists - list - a list of lists, each of which is the characters
            which have already acted in the initiative passes.
        mDelayedLists - list - a list of characters that have delayed their
            actions in the current initiative pass.
        mEndOfPassIndex - -1 unless it is the end of an initiative pass, 
            otherwise the index of the character on the delayed list who has a
            chance to act.
    """

    def __init__(self):
        """
        New, empty initiative tool.
        """
        self.mCharacters = set()
        self.mRound = 0
        self.mPassLists = [[]]
        self.mActedLists = [[]]
        self.mDelayedList = []
        self.mEndOfPassIndex = -1

    def addCharacter(self, char):
        """
        Add an SRCharacter to the combat. No effect if the character is
        already in the combat.  Has no effect on the current round.
        """
        self.mCharacters.add(char)

    def removeCharacter(self, char):
        """
        Remove an SRCharacter from the combat. No effect if the character is
        not already in the combat.  Also removes the character from all
        active initiative passes.
        """
        self.mCharacters.discard(char)
        for p in self.mPassLists:
            if p.count(char) > 0:
                p.remove(char)
        if self.mDelayedList.count(char) > 0:
            self.mDelayedList.remove(char)

    def startRound(self):
        """
        Roll initiatives for all characters and begin a combat round.
        Returns the character that will act first.
        """
        # TODO preserve delayed list between rounds
        self.mRound += 1
        self.mPass = 0
        self.mEndOfPassIndex = -1
        passes = 0

        # Roll all initiatives, check how many passes we have
        for char in self.mCharacters:
            char.rollNewInit()
            if char.mCurrentInitPasses > passes:
                passes = char.mCurrentInitPasses
                
        # Reset respective lists of initiative passes
        self.mPassLists = [[]]
        self.mActedLists = [[]]
        for i in range(1, passes):
            self.mPassLists.append([])
            self.mActedLists.append([])

        # Add each character to however many passes it's on
        for char in self.mCharacters:
            for p in range(0, char.mCurrentInitPasses):
                # Skip first round if this character is already delaying
                if p > 0 or self.mDelayedList.count(char) == 0:
                    self.mPassLists[p].append(char)

        # Sort the list for each initiative pass
        for pl in self.mPassLists:
            pl.sort(cmp = SRCharacter.initOrderComparator)

        if len(self.mPassLists[0]) == 0:
            if len(self.mDelayedList) > 0:
                self.mEndOfPassIndex = 0
                return self.mDelayedList[0]
            return None
        return self.mPassLists[0][0]

    def step(self):
        """
        Move on to the next character in the combat.  Returns the
        character that will act next, or None if this is the end of the
        round.  Will probably cause a crash if the round has already ended.
        """
        if self.mEndOfPassIndex < 0:
            # Regular actions in round
            actionList = self.mPassLists[self.mPass]
            self.mActedLists[self.mPass].append(actionList.pop(0))
            if len(actionList) == 0 or \
                    actionList[0].getInitScore() <= 0:
                while len(actionList) > 0:
                    self.mDelayedList.append(self.mPassLists[self.mPass].pop(0))
                if len(self.mDelayedList) == 0:
                    # No delayed actions; next pass
                    self.mPass += 1
                else:
                    # Actions delayed to end of pass
                    self.mEndOfPassIndex = 0;
                    self.mDelayedList.sort(cmp = SRCharacter.initOrderTiebreaker)
        else:
            # Delayed actions in pass
            actionList = self.mDelayedList
            self.mActedLists[self.mPass].append(actionList.pop(self.mEndOfPassIndex))
            if len(actionList) <= self.mEndOfPassIndex:
                # Next pass; any actions still delayed override actions that pass
                self.mPass += 1
                self.mEndOfPassIndex = -1
                if self.mPass < len(self.mPassLists):
                    actionList = self.mPassLists[self.mPass]
                    for char in self.mDelayedList:
                        if actionList.count(char) > 0:
                            actionList.remove(char)
                
        for pl in self.mPassLists:
            pl.sort(cmp = SRCharacter.initOrderComparator)
        
        return self.getCurrentCharacter()
    
    def delay(self):
        """
        Move the currently acting character to the delayed list for the time
        being.  Returns the character that will act next, or None if this is
        the end of the round.
        """
        if self.mEndOfPassIndex < 0:
            # Regular actions in round
            actionList = self.mPassLists[self.mPass]
            self.mDelayedList.append(actionList.pop(0))
            if len(actionList) == 0 or \
                    actionList[0].getInitScore() <= 0:
                while len(actionList) > 0:
                    self.mDelayedList.append(self.mPassLists[self.mPass].pop(0))
                if len(self.mDelayedList) == 0:
                    # No delayed actions; next pass
                    self.mPass += 1
                else:
                    # Actions delayed to end of pass
                    self.mEndOfPassIndex = 0;
                    self.mDelayedList.sort(cmp = SRCharacter.initOrderTiebreaker)
        else:
            # Delayed actions in pass
            actionList = self.mDelayedList
            self.mEndOfPassIndex += 1
            if len(actionList) <= self.mEndOfPassIndex:
                # Next pass; any actions still delayed override actions that pass
                self.mPass += 1
                self.mEndOfPassIndex = -1
                if self.mPass < len(self.mPassLists):
                    actionList = self.mPassLists[self.mPass]
                    for char in self.mDelayedList:
                        if actionList.count(char) > 0:
                            actionList.remove(char)
                    if len(actionList) == 0:
                        self.mEndOfPassIndex = 0
                
        for pl in self.mPassLists:
            pl.sort(cmp = SRCharacter.initOrderComparator)
        
        return self.getCurrentCharacter()
    
    def intervene(self, char):
        """
        Move a character from the delayed list to the acted list for the current
        pass.  Does nothing if the character is not in fact on the delayed list.
        """
        if self.mDelayedList.count(char) > 0:
            charIndex = self.mDelayedList.index(char)
            if self.mEndOfPassIndex > charIndex:
                self.mEndOfPassIndex -= 1
            self.mDelayedList.remove(char)
            if self.mPass < len(self.mActedLists):
                self.mActedLists[self.mPass].append(char)
            else:
                self.mActedLists[-1].append(char)

    def getCurrentCharacter(self):
        """
        Return the currently acting character, or None if the round has
        ended.
        """
        if self.mPass >= len(self.mPassLists):
            return None
        if self.mEndOfPassIndex >= 0:
            return self.mDelayedList[self.mEndOfPassIndex]
        return self.mPassLists[self.mPass][0]

##########
# Tkinter GUI
##########

from Tkinter import *
from ScrolledText import ScrolledText
import tkFileDialog
import tkFont

class CharacterInitFrame(Frame):
    """
    A widget class to display pertinent information about a character in
    an initiative tool. AKA: Name, init dice, init passes, physical
    damage, and stun damage.

    This is for displaying a list of characters and modifying their stats,
    not to put in the initiative order.
    """
    def __init__(self, master, char, tool):
        Frame.__init__(self, master,
                       relief=GROOVE,borderwidth=2)
        self.mChar = char
        self.mTool = tool
        tool.addCharacter(char)

        topFrame = Frame(self)
        self.mNameField = Text(topFrame, width=24, height=1)
        self.mNameField.insert(END, char.mName)
        self.mNameField.config(state=DISABLED)
        self.mNameField.pack(side=LEFT)

        Button(topFrame, text="Edit", command=self.edit).pack(side=LEFT)
        Button(topFrame, text="X", command=self.remove).pack(side=LEFT, padx=2)

        topFrame.pack(side=TOP)

        bottomFrame = Frame(self)

        diceFrame = Frame(bottomFrame)
        Label(diceFrame, text="Dice").pack()
        self.mDiceField = Text(diceFrame, width=2, height=1)
        self.mDiceField.pack()
        diceFrame.pack(side=LEFT)

        passFrame = Frame(bottomFrame)
        Label(passFrame, text="Passes").pack()
        self.mPassField = Text(passFrame, width=2, height=1)
        self.mPassField.pack()
        passFrame.pack(side=LEFT)

        self.mPhysVar = ValidatingIntVar(self.mChar.mPhysDamage)
        self.mStunVar = ValidatingIntVar(self.mChar.mStunDamage)

        def dmgcb(name, index, mode):
            if name == str(self.mPhysVar):
                self.mChar.mPhysDamage = self.mPhysVar.get()
            if name == str(self.mStunVar):
                self.mChar.mStunDamage = self.mStunVar.get()

        LabeledNumberEntry(bottomFrame, "Phys Dmg", 8,
                           self.mPhysVar, 0, 12,
                           dmgcb, False).pack(side=LEFT)
        LabeledNumberEntry(bottomFrame, "Stun Dmg", 8,
                           self.mStunVar, 0, 12,
                           dmgcb, False).pack(side=LEFT)

        bottomFrame.pack(side=BOTTOM)

        self.updateFields()

    def remove(self):
        """
        Remove this character from the initiative tool, and destroy the
        widget.
        """
        self.mTool.removeCharacter(self.mChar)
        self.destroy()

    def edit(self):
        """
        Open a new CharacterEditorFrame window for this character.
        """
        dialog = Toplevel()
        dialog.wm_title("Editing character")
        dialog.wm_resizable(False, False)
        editor = CharacterEditorFrame(dialog, self.mChar)
        editor.pack()
        #dialog.transient(self.winfo_toplevel())
        dialog.focus_set()
        dialog.grab_set()
        self.wait_window(dialog)
        self.updateFields()

    def doDamage(self, phys=0, stun=0):
        """
        Apply the given damage to the character, and update the display.
        """
        self.mChar.doDamage(phys, stun)
        self.updateFields()

    def updateFields(self):
        """
        Ensure that the displayed fields all match what's in the character.
        """
        t = self.mNameField
        t.config(state=NORMAL)
        t.delete(1.0, END)
        t.insert(END, self.mChar.mName)
        t.config(state=DISABLED)

        t = self.mDiceField
        t.config(state=NORMAL)
        t.delete(1.0, END)
        t.insert(END, str(self.mChar.getInitDice()))
        t.config(state=DISABLED)

        t = self.mPassField
        t.config(state=NORMAL)
        t.delete(1.0, END)
        t.insert(END, str(self.mChar.getInitPasses()))
        t.config(state=DISABLED)

        self.mPhysVar.set(self.mChar.mPhysDamage)
        self.mStunVar.set(self.mChar.mStunDamage)


class CharacterListFrame(Frame):
    """
    A widget class maintaining a set of characters, and a
    CharacterInitFrame for each one.
    """
    def __init__(self, master, tool):
        """
        Create a CharacterListFrame and attach it to an SRInitTool.
        """
        Frame.__init__(self, master)
        btnFrame = Frame(self)
        addButton = Button(btnFrame, text="Add new character",
                           command = self.newCharacter)
        addButton.pack(side=LEFT)
        loadButton = Button(btnFrame, text="Load character",
                            command = self.loadCharacter)
        loadButton.pack(side=RIGHT)
        btnFrame.pack(side=TOP)
        self.mCount = 0
        self.mTool = tool

    def newCharacter(self):
        """
        Add a new character with default settings to the initiative
        list.  Used as a callback by the Add button.
        """
        self.mCount += 1
        char = SRCharacter("New character " + str(self.mCount))

        cframe = CharacterInitFrame(self, char, self.mTool)
        cframe.pack(side=TOP, pady=3) # A kludge until I figure out how to do this in CharacterInitFrame
        cframe.edit()

    def loadCharacter(self):
        """
        Add a new character, loaded from a file.
        """
        filename = tkFileDialog.askopenfilename(
            defaultextension = ".s4c",
            filetypes = [("Sr4toolkit character files", "*.s4c"),
                         ("All files", "*.*")],
            parent = root,
            title = "Open Character...")
        if filename == '':
            return
        
        fileIn = open(filename)
        char = loadSRCharacter(fileIn)

        cframe = CharacterInitFrame(self, char, self.mTool)
        cframe.pack(side=TOP, pady=3) # A kludge until I figure out how to do this in CharacterInitFrame

class InitRoundFrame(Frame):
    """
    A widget class that runs an SRInitTool, maintains rounds and
    passes, and interprets the tool output.
    """
    def __init__(self, master, tool):
        Frame.__init__(self, master)
        self.mTool = tool

        buttonFrame = Frame(self)
        self.mNewRoundButton = Button(buttonFrame, text="Start new round",
                                      command=self.newRound)
        self.mNewRoundButton.pack(side=LEFT)
        
        self.mDelayButton = Button(buttonFrame, text="Delay action",
                                   command=self.delay, state=DISABLED)
        self.mDelayButton.pack(side=RIGHT)

        self.mStepButton = Button(buttonFrame, text="Step to next action",
                                  command=self.step, state=DISABLED)
        self.mStepButton.pack(side=RIGHT)
        buttonFrame.pack(side=TOP)

        boldFont = tkFont.Font(weight="bold", size=9)
        self.mText = ScrolledText(self, wrap=WORD, relief=GROOVE, borderwidth=2)
        self.mText.tag_config("bold", font=boldFont)
        self.mText.tag_config("blue", foreground="blue", underline=1)
        self.mText.tag_bind("blue", "<Button-1>", self.textClickCB)
        self.mText.insert(END, "No current round")
        self.mText.config(state=DISABLED)
        self.mText.pack(side=TOP, fill=BOTH)
        
        # This will map character names to character objects, so we can use 
        # text tags to retrieve characters from the delayed list
        self.mCharDict = dict()
    
    def setActiveButtons(self, midRound):
        """
        Set the start round, delay, and step buttons to active or inactive as
        appropriate, for whether the tool is in a round or not.
        """
        if midRound:
            self.mStepButton.config(state=NORMAL)
            self.mDelayButton.config(state=NORMAL)
            self.mNewRoundButton.config(state=DISABLED)
        else:
            self.mStepButton.config(state=DISABLED)
            self.mDelayButton.config(state=DISABLED)
            self.mNewRoundButton.config(state=NORMAL)

    def newRound(self):
        """
        Start a new combat round.  If there are actually characters,
        enable the step button.
        """
        char = self.mTool.startRound()
        self.updateText()
        self.setActiveButtons(char != None)

    def step(self):
        """
        Move on to the next character in the combat.
        """
        char = self.mTool.step()
        self.updateText()
        if char == None:
            self.setActiveButtons(False)
    
    def delay(self):
        """
        Delay the current character in the combat.
        """
        char = self.mTool.delay()
        self.updateText()
        if char == None:
            self.setActiveButtons(False)
    
    def intervene(self, char):
        """
        Let a character from the delayed list have an intervention action.
        """
        self.mTool.intervene(char)
        self.updateText()
        
    def textClickCB(self, event):
        """
        Callback for when the user clicks on a deferred character.
        """
        tags = event.widget.tag_names("@%d,%d" % (event.x, event.y))
        for t in tags:
            if t.startswith("char://"):
                if self.mCharDict.has_key(t):
                    self.intervene(self.mCharDict[t])

    def updateText(self):
        """
        Read the characters from the initiative tool and display them,
        along with their initiative scores, in the text field.
        """
        self.mText.config(state=NORMAL)
        self.mText.delete(1.0, END)

        actingChar = self.mTool.getCurrentCharacter()
        if actingChar == None:
            self.mText.insert(END, "End of round " + str(self.mTool.mRound))
        else:
            self.mText.insert(END, "Current round: " + str(self.mTool.mRound))
            self.mText.insert(END, "\nCurrent pass: " + str(self.mTool.mPass + 1))
            self.mText.insert(END, "\nCurrently acting: " + str(actingChar.mName))

        for passNo in range(0, len(self.mTool.mPassLists)):
            al = self.mTool.mActedLists[passNo]
            dl = self.mTool.mDelayedList
            pl = self.mTool.mPassLists[passNo]
            self.mText.insert(END, "\n\nInitiative pass " + str(passNo + 1))
            printedActingChar = False
            for char in al:
                self.mText.insert(END, "\n" + char.mName)
            if passNo == self.mTool.mPass:
                for char in dl:
                    self.mText.insert(END, "\n")
                    tags = None
                    if char.getInitScore() > 0 or not printedActingChar:
                        self.mText.insert(END, "(Delayed) ")
                        tags = ("blue", "char://" + str(char))
                        self.mCharDict["char://"+str(char)] = char
                    else:
                        self.mText.insert(END, "(End) ")
                    if char == actingChar:
                        tags = ("bold")
                        printedActingChar = True
                    self.mText.insert(END, char.mName, tags)
            for char in pl:
                self.mText.insert(END, "\n")
                score = char.getInitScore()
                if (score > 0):
                    textEntry = char.mName + " [" + \
                                str(score) + "]"
                else:
                    textEntry = "(End) " + char.mName
                tags = None
                if char == actingChar and passNo == self.mTool.mPass:
                    tags = ("bold")
                    printedActingChar = True
                self.mText.insert(END, textEntry, tags)
                
        self.mText.config(state=DISABLED)
    

if __name__== "__main__":
    root = Tk()
    root.wm_resizable(False, False)
    root.wm_title("Shadowrun 4 Initiative Tool")

    menubar = Menu(root)
    helpmenu = Menu(menubar, tearoff=0)
    helpmenu.add_command(label="About...",
                         command=lambda: showAboutWindow(root))
    
    menubar.add_cascade(label="Help", menu=helpmenu)
    root.config(menu=menubar)
    
    frame = Frame(root)

    tool = SRInitTool()
    charListFrame = CharacterListFrame(frame, tool)
    charListFrame.pack(side=LEFT, fill=Y, padx=3)

    roundFrame = InitRoundFrame(frame, tool)
    roundFrame.pack(side=RIGHT, fill=BOTH, padx=3)

    frame.pack(fill=BOTH)

    root.mainloop()

        
